﻿// Dump.cs
//
// Copyright 2008-2011 by Burt Harris
// Licensed under the Microsoft Public License (Ms-PL)
// See: http://www.codeplex.com/yaml/license
//

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
#if CLR2
using Microsoft.Scripting.Utils;
#endif

namespace YaTools.Yaml
{
    using DumpAction = Action<Dump, object>;
    using DumpActionTable = Dictionary<Type, Action<Dump, object>>;

    /// <summary>
    /// Implements Yaml Dump (serialization) operation.
    /// </summary>
    [DebuggerDisplay("W: {_writer}")]
    public sealed partial class Dump 
    {
        [Flags]
        public enum Pending : short
        {
            // Pending operations will be performed from MSB to LSB
            DocumentHeader = 1 << 14,
            EndDirectives = 1 << 13,
            BreakBefore = 1 << 12,
            Space = 1 << 11,
            Indent = 1 << 10,
            BeginSequence = 1 << 9,
            EndSequence = 1 << 8,
            BeginMapping = 1 << 7,
            EndMapping = 1 << 6,
            ListDelimiter = 1 << 5,
            KeyDelimiter = 1 << 4,
            SpaceAfter = 1 << 3,
            BreakAfter = 1 << 2,
            IndentAfter = 1 << 1,
            DocumentEnd = 1 << 0,
            Nothing = 0
        }

        [ContractClass(typeof (AbstractContracts.DecoratorContract))]
        public abstract partial class Decorator
        {
            public abstract bool PresentNode(Dump dump, object value);
            public abstract void Preprocess(Dump dump, object value);
            public abstract string HandleForTag(string tag);

            protected void PresentAlias(Dump dump, string anchor)
            {
                Contract.Requires(dump != null);
                Contract.Requires(anchor != null);
                dump._active.Presenter.PresentAlias(dump, anchor);
            }

            protected void PresentAnchor(Dump dump, string anchor)
            {
                Contract.Requires(dump != null);
                Contract.Requires(anchor != null);
                dump._active.Presenter.PresentAnchor(dump, anchor);
            }

            protected void PresentNode(Dump dump, Tag tag)
            {
                Contract.Requires(dump != null);
                Contract.Requires(tag != null);
                dump._active.Presenter.PresentTag(dump, tag);
            }
        }

        [ContractClass(typeof (AbstractContracts.PresenterContract))]
        public abstract partial class Presenter
        {
            /// <summary>
            /// Write a null node
            /// </summary>
            /// <param name="dump"></param>
            public abstract void PresentNull(Dump dump);

            /// <summary>
            /// Dump a Scalar value 
            /// </summary>
            /// <param name="dump">Active Dump</param>
            /// <param name="value">Scalar value</param>
            /// <param name="tag"></param>
            public abstract void PresentScalar(Dump dump, string value, Tag tag);

            public abstract void PresentAlias(Dump dump, string anchor);

            public abstract void PresentAnchor(Dump dump, string anchor);

            public virtual void PresentTag(Dump dump, Tag tag) {}
        }

        private static readonly string[] DefaultSortNames = new[] { "Name" };

        private Pending _pending;
        private TextWriter _writer;
        private Decorator _decorator;
        private readonly DumpActionTable _dumpActions;
        private readonly StreamingContext _streamingContext;
        private readonly SurrogateSelector _surrogateSelector = new SurrogateSelector();

        private readonly Stack<Waypoint> _waypointStack = new Stack<Waypoint>();
        private Waypoint _active;
        private FormatterConverter _formatterConverter;
        private int _indentFactor = 2;

        public Dump(
            Presenter presenter = null,
            Decorator decorator = null,
            StreamingContextStates states = StreamingContextStates.All)
        {
            Contract.Ensures(_decorator != null);
            Contract.Ensures(_waypointStack != null);
            Contract.Ensures(_dumpActions != null);
            Contract.Ensures(_writer != null);

            _writer = TextWriter.Null;
            _active = new Waypoint { Presenter = presenter ?? new Dump.Presenter.Yaml.Explicit() };
            _decorator = decorator ?? new Decorator.Yaml();
            _streamingContext = new StreamingContext(states, this);
            _dumpActions = new DumpActionTable();
            _pending = Pending.DocumentHeader;

            InitializeDumpActions(_dumpActions);
            ResetProperties();
        }

        // ReSharper restore UnusedMember.Local

        public int Depth
        {
            [Pure]
            get { return _waypointStack.Count; }
        }

        public string Anchor
        {
            get { return _active.Anchor; }
            set { _active.Anchor = value; }
        }

        public Type ExpectedType { get; set; }

        public int NestingLevel
        {
            get { return _active.Nesting; }
        }

        public int Count
        {
            get { return _active.Count; }
        }

        public bool IsKey
        {
            get { return _active.IsKey; }
            private set { _active.IsKey = value; }
        }

        public bool PreviousWasKey
        {
            get { return _active.PreviousWasKey; }
        }

        public Kind Kind
        {
            get { return _active.Kind; }
            private set { _active.Kind = value; }
        }

        private void InitializeDumpActions(DumpActionTable dumpActions)
        {
            Contract.Requires(dumpActions != null);

            dumpActions.Add(typeof (string), (dumper, value) =>
                dumper.DumpScalar((string) value, Tag.String));
            dumpActions.Add(typeof (char), (dumper, value) =>
                dumper.DumpScalar(value, value.ToString(), Tag.String));
            dumpActions.Add(typeof (bool), (dumper, value) =>
                dumper.DumpScalar(value, ((bool)value)
                ? "true"
                : "false", Tag.Boolean));

            DumpAction intAction = (dumper, value) =>
                dumper.DumpScalar(value, value.ToString(), Tag.Integer);

            dumpActions.Add(typeof (Byte), intAction);
            dumpActions.Add(typeof (SByte), intAction);
            dumpActions.Add(typeof (Int16), intAction);
            dumpActions.Add(typeof (Int32), intAction);
            dumpActions.Add(typeof (Int64), intAction);
            dumpActions.Add(typeof (UInt16), intAction);
            dumpActions.Add(typeof (UInt32), intAction);
            dumpActions.Add(typeof (UInt64), intAction);
            dumpActions.Add(typeof (float), (dumper, value) =>
                                 {
                                     var real = (float) value;
                                     string rep;
                                     if (float.IsNegativeInfinity(real)) rep = "-.Inf";
                                     else if (float.IsPositiveInfinity(real)) rep = "+.Inf";
                                     else if (float.IsNaN(real)) rep = ".NaN";
                                     else rep = real.ToString();
                                     dumper.DumpScalar(value, rep, Tag.Float);
                                 });

            dumpActions.Add(typeof (double), (dumper, value) =>
                                 {
                                     var real = (double) value;
                                     string rep;
                                     if (double.IsNegativeInfinity(real)) rep = "-.Inf";
                                     else if (double.IsPositiveInfinity(real)) rep = "+.Inf";
                                     else if (double.IsNaN(real)) rep = ".NaN";
                                     else rep = real.ToString();
                                     dumper.DumpScalar(value, rep, Tag.Float);
                                 });

            dumpActions.Add(typeof (byte[]), (dumper, value) =>
                dumper.DumpScalar(value, Convert.ToBase64String(((byte[]) value),
                    Base64FormattingOptions.None), Tag.Binary));

            dumpActions.Add(typeof (DateTime), (dumper, value) =>
                {
                    var dateTime = (DateTime) value;
                    string format = null;

                    switch (dateTime.Kind)
                    {
                        case DateTimeKind.Local:
                            format = "yyyy-MM-dd HH:mm:ss z";
                            break;

                        case DateTimeKind.Unspecified:
                            // Sort fo a mess, but Yaml will interpret it as 
                            // UTC, so lets make it official.
                        case DateTimeKind.Utc:
                            format = "yyyy-MM-dd HH:mm:ss Z";
                            break;
                    }

                    dumper.DumpScalar(value,
                            dateTime.ToString(format, CultureInfo.InvariantCulture), Tag.Timestamp);
                });

            dumpActions.Add(typeof (IntPtr), DumpConverted);
            dumpActions.Add(typeof (decimal), DumpConverted);
            dumpActions.Add(typeof (Guid), DumpConverted);
            dumpActions.Add(typeof (TimeSpan), DumpConverted);
            dumpActions.Add(typeof (Uri), DumpConverted);

            dumpActions.Add(typeof (TaggedScalar), (dumper, value) =>
                                 {
                                     var scalar = (TaggedScalar) value;
                                     dumper.DumpScalar(value, scalar.Value, scalar.Tag);
                                 });

            dumpActions.Add(typeof (TaggedDictionary), (dumper, value) =>
                                 {
                                     var map = (TaggedDictionary) value;
                                     DumpIDictionary(dumper, map);
                                 });

            dumpActions.Add(typeof (TaggedSequence), (dumper, value) =>
                                 {
                                     var seq = (TaggedSequence) value;
                                     dumper.DumpList(seq);
                                 });
        }

        [ContractInvariantMethod]
        // ReSharper disable UnusedMember.Local
        private void ObjectInvariant()
        {
            Contract.Invariant(this._waypointStack != null);
            Contract.Invariant(this.Depth >= 0);
            Contract.Invariant(this.NestingLevel >= 0);
            Contract.Invariant(this._indentFactor >= 0);
            Contract.Invariant(this._active.Presenter != null);
            Contract.Invariant(this._dumpActions != null);
            Contract.Invariant(this._writer != null);
            Contract.Invariant(this._decorator != null);
        }
        // ReSharper restore UnusedMember.Local

        private void PushActive()
        {
            Contract.Ensures(this.Depth == 1 + Contract.OldValue<int>(this.Depth));
            _waypointStack.Push(_active);
        }

        private void WriteSeperation()
        {
            while (_pending != Pending.Nothing)
                _active.Presenter.WriteSeperation(this);
        }

        public void Write(char c)
        {
            WriteSeperation();
            _writer.Write(c);
            _pending = c == ' ' ? Pending.Nothing : Pending.Space;
        }


        public void Write(string s)
        {
            WriteSeperation();
            _writer.Write(s);
            _pending = Pending.Space;
        }

        public void Write(char sigil, string s)
        {
            WriteSeperation();
            _writer.Write(sigil);
            _writer.Write(s);
            _pending = Pending.Space;
        }

        public void WriteLine()
        {
            _pending |= Pending.BreakAfter;
            WriteSeperation();
            _writer.WriteLine();
            _pending = Pending.Indent;
        }

        public void WriteLine(char c)
        {
            _pending |= Pending.BreakAfter;
            WriteSeperation();
            _writer.WriteLine(c);
            _pending = Pending.Indent;
        }

        public void WriteLine(string p)
        {
            _pending |= Pending.BreakAfter;
            WriteSeperation();
            _writer.WriteLine(p);
            _pending = Pending.Indent;
        }

        public void WriteYamlScalar(object value, Scalar.Style style, Scalar.Chomping chomping, bool flow)
        {
            Contract.Requires(value != null);

            Scalar.EmitScalar(
                writer: _writer,
                style: style,
                forceWidth: 0,
                chomping: chomping,
                str: value.ToString(),
                flow: flow,
                indent: IndentSize());
        }

        private void ResetProperties()
        {
            _active.Anchor = null;
            ExpectedType = null;
            if (_active.Kind == Kind.Mapping && !_active.IsKey && !_active.PreviousWasKey)
                _active.IsKey = true;
        }

        public void BeginMap(object value)
        {
            Contract.Requires(value != null);

            PushActive();
            Kind = Kind.Mapping;
            _active.IsKey = true;
            _active.PreviousWasKey = false;
            _active.Count = 0;
            _active.Nesting++;
            _pending |= Pending.BeginMapping;
            ResetProperties();
        }

        public void BeginSeq(object value)
        {
            Contract.Requires(value != null);

            PushActive();
            Kind = Kind.Sequence;
            _active.Count = 0;
            _active.IsKey = false;
            _active.PreviousWasKey = false;
            _active.Nesting++;
            _pending |= Pending.BeginSequence | Pending.Space;
            ResetProperties();
        }

        public bool BeginDocument()
        {
            PushActive(); // Does this belong?
            // Contract.Assert(this.Depth == 1);
            return true;
        }

        public void EndMapping()
        {
            Contract.Assume(this.Depth > 0);
            Waypoint saved = _waypointStack.Pop();
            if (saved.Presenter != null)
            {
                _active.Anchor = saved.Anchor;
                _pending = Pending.EndMapping;
                _active = saved;
                _active.Count++;
                _active.PreviousWasKey = _active.IsKey;
            }
            ResetProperties();
        }

        public void EndSequence()
        {
            Contract.Assume(this.Depth > 0);
            Waypoint saved = _waypointStack.Pop();
            if (saved.Presenter != null)
            {
                _active.Anchor = saved.Anchor;
                _pending |= Pending.EndSequence;
                _active = saved;
                _active.Count++;
                _active.PreviousWasKey = _active.IsKey;
            }
            ResetProperties();
        }

        public void DumpScalar(string value, Tag tag)
        {
            Contract.Requires(value != null);

            if (_decorator.PresentNode(this, value))
            {
                _active.Presenter.PresentScalar(this, value, tag);
            }
            _active.Count++;
            _active.PreviousWasKey = _active.IsKey;
            _active.IsKey = false;
            ResetProperties();
        }

        public void DumpScalar(object actualValue, string value, Tag tag)
        {
            Contract.Requires(tag != null);
            Contract.Requires(actualValue != null);
            Contract.Requires(value != null);

            if (_decorator.PresentNode(this, actualValue))
            {
                _active.Presenter.PresentScalar(this, value, tag);
            }
            _active.Count++;
            _active.PreviousWasKey = _active.IsKey;
            _active.IsKey = false;
            ResetProperties();
        }

        internal void DumpValue(object value, Type expectedType=null)
        {
            ExpectedType = expectedType ?? typeof(object);

            if (value == null)
            {
                _active.Presenter.PresentNull(this);
                _pending = Pending.ListDelimiter;
            }
            else
            {
                if (_decorator.PresentNode(this, value))
                {
                    try
                    {
                        DumpAction action;
                        Type theType = value.GetType();

                        // Try looking up the action in the cache.

                        if (!_dumpActions.TryGetValue(theType, out action))
                        {
                            action = GenerateDumpAction(theType, value);
                            if (action != null)
                                _dumpActions.Add(theType, action);
                        }
                        if (action != null) action(this, value);
                    }
                    finally
                    {
                        _pending = Pending.ListDelimiter;
                    }
                }
            }
        }

        public string ToString(object value, Type expectedType=null)
        {
            var writer = new StringWriter();
            new Dump().Serialize(writer, value, expectedType);
            return writer.ToString();
        }

        public void Serialize(TextWriter writer, object value, Type expectedType = null)
        {
            TextWriter temp = this._writer;
            try
            {
                ExpectedType = expectedType ?? typeof(object);

                if (value == null)
                {
                    _active.Presenter.PresentNull(this);
                    _pending = Pending.ListDelimiter;
                }
                else
                {
                    _decorator.Preprocess(this, value);
                    _writer = writer;
                    if (_decorator.PresentNode(this, value))
                    {
                        try
                        {
                            DumpAction action;
                            Type theType = value.GetType();

                            // Try looking up the action in the cache.

                            if (!_dumpActions.TryGetValue(theType, out action))
                            {
                                action = GenerateDumpAction(theType, value);
                                if (action != null)
                                    _dumpActions.Add(theType, action);
                            }
                            if (action != null) action(this, value);
                        }
                        finally
                        {
                            _pending = Pending.ListDelimiter;
                        }
                    }
                }
            }
            finally
            {
                this._writer = temp;
            }
        }

        public string HandleForTag(string tag)
        {
            Contract.Requires(tag != null);
            Contract.Ensures(Contract.Result<string>() != null);

            return _decorator.HandleForTag(tag);
        }

        private static Tag TagForScalar(object value)
        {
            Contract.Requires(value != null);
            var x = TypeDescriptor.GetClassName(value);
            return new Tag("tag:yaml.codeplex.org,2010:1:" + x, Kind.Scalar);
        }

        private DumpAction GenerateDumpAction(Type type, object value)
        {
            Contract.Requires(value != null);
            Contract.Requires(_surrogateSelector != null);

            ISurrogateSelector selector;
            ISerializationSurrogate surrogate = _surrogateSelector.GetSurrogate(type, _streamingContext, out selector);


            DumpAction action;
            AttributeCollection attributes = TypeDescriptor.GetAttributes(value);
            Contract.Assume(attributes != null);


            if (attributes.Contains(YamlScalarAttribute.Yes))
                action = (dumper,v) => DumpScalar(
                    v.ToString(), 
                    TagForScalar(TypeDescriptor.GetClassName(value)));
            else if (value is NameObjectCollectionBase)
                action = DumpNameObjectCollection;
            else if (value is StringDictionary)
                action = DumpStringDictionary;
            else if (value is IOrderedDictionary)
                action = DumpIOrderedDictionary;
            else if (value is IDictionary)
                action = DumpIDictionary;
            else if (value is IEnumerable)
                action = DumpIEnumerable;
            else if (value is Enum)
                action = DumpConverted;
            else if (value is ISerializable)
                action = DumpISerializable;
            else
            {
                TypeConverter converter = TypeDescriptor.GetConverter(value);
                if (converter != null && converter.CanConvertFrom(typeof (string)))
                {
                    action = (dumper, v) => DumpScalar(
                        converter.ConvertToInvariantString(v), 
                        TagForScalar(TypeDescriptor.GetClassName(value)));
                }
                else
                {
                    action = DumpObject;
                }
            }
            return action;
        }

        internal void DumpPair(object key, Type expectedKeyType,
                               object value, Type expectedValueType)
        {
            bool wasKey = IsKey;
            try
            {
                IsKey = true;
                DumpValue(key, expectedKeyType);
                _pending = Pending.KeyDelimiter;
                try
                {
                    DumpValue(value, expectedValueType);
                }
                catch (Exception)
                {
                    DumpValue(null, typeof (object));
                    throw;
                }
            }
            finally
            {
                IsKey = wasKey;
            }
        }

        private static void DumpConverted(Dump dump, object value)
        {
            Contract.Requires(dump != null);
            Contract.Requires(value != null);

            dump.DumpScalar(Convert.ToString(value), TagForScalar(value));
        }

        private static void DumpIDictionary(Dump dump, object value)
        {
            Contract.Requires(dump != null);
            Contract.Requires(value != null);

            var dictionary = (IDictionary) value;

            dump.BeginMap(dictionary);
            try
            {
                var expectedKeyType = dictionary.Keys.GetType().GetElementType();
                var expectedValueType = dictionary.Values.GetType().GetElementType();

                ICollection keys = dictionary.Keys;
                foreach (object key in SortKeys(keys))
                {
                    Contract.Assume(key != null);
                    dump.DumpPair(key, expectedKeyType, dictionary[key], expectedValueType);
                }
            }
            finally
            {
                dump.EndMapping();
            }
        }

        private static void DumpNameObjectCollection(Dump dump, object value)
        {
            Contract.Requires(dump != null);
            Contract.Requires(value != null);

            var collection = (NameObjectCollectionBase) value;

            dump.BeginSeq(collection);
            {
                try
                {
                    NameObjectCollectionBase.KeysCollection keys = collection.Keys;
                    int i = 0;
                    foreach (object o in collection)
                    {
                        dump.DumpPair(keys[i++], typeof(string), o, typeof(object));
                    }
                }
                finally
                {
                    dump.EndSequence();
                }
            }
        }

        private void DumpIOrderedDictionary(Dump dump, object value)
        {
            Contract.Requires(dump != null);
            Contract.Requires(value != null);

            var omap = (IOrderedDictionary) value;
            var expectedKeyType = omap.Keys.GetType().GetElementType();
            var expectedValueType = omap.Values.GetType().GetElementType();

            dump.BeginSeq(omap);
            try
            {
                IDictionaryEnumerator e = omap.GetEnumerator();
                while (e.MoveNext())
                {
                    dump.DumpPair(e.Key, expectedKeyType, e.Value, expectedValueType);
                }
            }
            finally
            {
                dump.EndSequence();
            }
        }

        private void DumpStringDictionary(Dump dump, object value)
        {
            Contract.Requires(dump != null);
            Contract.Requires(value != null);

            var dictionary = (StringDictionary) value;

            dump.BeginSeq(dictionary);
            try
            {
                foreach (string key in SortKeys(dictionary.Keys))
                    dump.DumpPair(key, typeof(string),
                             dictionary[key], typeof (object));
            }
            finally
            {
                dump.EndSequence();
            }
        }

        private void DumpList(object value)
        {
            Contract.Requires(value != null);

            var list = (IList) value;

            BeginSeq(list);
            try
            {
                foreach (object v in list)
                {
                    DumpValue(v);
                }
            }
            finally
            {
                EndSequence();
            }
        }

        private void DumpIEnumerable(Dump dump, object value)
        {
            Contract.Requires(dump != null);
            Contract.Requires(value != null);

            var enumerable = (IEnumerable) value;
            dump.BeginSeq(enumerable);
            try
            {
                foreach (object v in enumerable)
                {
                    dump.DumpValue(v);
                }
            }
            finally
            {
                dump.EndSequence();
            }
        }

        private void DumpISerializable(Dump dump, object value)
        {
            Contract.Requires(dump != null);
            Contract.Requires(value != null);

            if (this._formatterConverter == null) _formatterConverter = new FormatterConverter();

            var s = (ISerializable) value;
            var info = new SerializationInfo(value.GetType(), _formatterConverter);
            s.GetObjectData(info, _streamingContext);

            //Tag = string.Format("!{0}#ISerializable", info.FullTypeName);
            dump.BeginMap(value);
            try
            {
                foreach (SerializationEntry pair in info)
                {
                    dump.DumpPair(pair.Name, typeof(string), pair.Value, pair.ObjectType);
                }
            }
            finally
            {
                dump.EndMapping();
            }
        }

        private static bool IsEmptyCollection(object component)
        {
            var collection = component as ICollection;
            if (collection != null && collection.Count == 0) return true;
            return false;
        }

        private static IEnumerable<object> SortKeys(ICollection keys)
        {
            Contract.Requires(keys != null);

            var array = new object[keys.Count];
            keys.CopyTo(array, 0);
            try
            {
                Array.Sort(array);
            }
            catch (InvalidOperationException)
            {
            }
            return array;
        }

        private static void DumpObject(Dump dump, object component)
        {
            Contract.Requires(dump != null);
            Contract.Requires(component != null);

            //Tag = "!" + TypeDescriptor.GetClassName(component);

            dump.BeginMap(component);
            try
            {
                PropertyDescriptor defaultProperty = TypeDescriptor.GetDefaultProperty(component);
                PropertyDescriptorCollection properties =
                    TypeDescriptor.GetProperties(component)
                        .Sort(DefaultSortNames);
                Contract.Assume(properties != null);

                if (defaultProperty != null)
                {
                    //Tag = "tag:yaml.org,2002:value";
                    dump.DumpPair(defaultProperty.Name, typeof(string), defaultProperty.GetValue(component),
                             defaultProperty.PropertyType);
                }

                for (int i = 0; i < properties.Count; i++)
                {
                    PropertyDescriptor property = properties[i];
                    if (property == defaultProperty) continue;
                    Contract.Assume(property != null);

                    try
                    {
                        object value = property.GetValue(component);

                        // TODO: better options control: Work item 9842.
                        if (!IsEmptyCollection(value)
                            && (property.ShouldSerializeValue(component)
                                || property.SerializationVisibility !=
                                DesignerSerializationVisibility.Hidden && value != null
                               ))
                        {
                            dump.DumpPair(
                                property.Name, typeof (string),
                                value, property.PropertyType);
                        }
                    }
                    catch (TargetInvocationException ex)
                    {
                        dump._active.Presenter.PresentComment(dump,
                                                     string.Format(" Error serializing propererty {0}: {1}",
                                                                   property.Name, ex.Message));

                        // Just move on !?!?
                    }
                }
            }
            finally
            {
                dump.EndMapping();
            }
        }

        private int IndentSize()
        {
            Contract.Ensures(Contract.Result<int>() >= 0);

            return this.NestingLevel * _indentFactor;
        }

        public void WriteDoubleQuoted(string value)
        {
            Contract.Requires(value != null);
            WriteSeperation();
            Scalar.WriteDoubleQuoted(this._writer, 80, value, IndentSize());
            _pending = Pending.Space;
        }

        #region Nested type: Waypoint

        private struct Waypoint
        {
            public string Anchor;
            public int Count;
            public bool IsKey;
            public Kind Kind;
            public int Nesting;
            public Presenter Presenter;
            public bool PreviousWasKey;
        }

        #endregion
    }
}